/**
 * @author مۇختەرجان مەخمۇت (mutearjan mahmood)
 * @email yeganaaa@163.com
 * @create date 2020-10-06 11:54:49
 * @modify date 2020-10-06 11:54:49
 * @desc [description]
*/
import {v4 as uuid} from "uuid"
import {BridgeHostEvents, Completer, Fragment, IRpcBridgeHost, IRpcServerHost, ServerHostEvents} from "../Exports";

export class KeepAliveRpcServerHost implements IRpcServerHost<IRpcServerHost<any>> {
    private timerId: NodeJS.Timeout = null!;
    public bridges: Map<string, KeepAliveRpcBridgeHost> = new Map();
    /**
     * constructor of {@link KeepAliveRpcServerHost}
     * @param backend Backend rpc server host.
     * @param heartbeatPeriodicTimeMillisecond  send every heartbeat periodic time on server side.
     * @param heardBeatTimeoutMilliseconds receive every heartbeat periodic timeout on client side, because some network problem (latency, slow and high timeout) causes client can't receive some heartbeat message in specified periodic time, this parameter is used to wait for receive heartbeat message after heartbeat periodic time end. this parameter is affect to client side.
     */
    public constructor(public backend: IRpcServerHost<any>, public heartBeatPeriodicTimeMillisecond: number = 60 * 1000, public heardBeatTimeoutMilliseconds: number = 1 * 1000) {
        if (this.heardBeatTimeoutMilliseconds * 2 >= this.heartBeatPeriodicTimeMillisecond) throw new Error('"heartBeatPeriodicTimeMillisecond" is should be greater then 2 times "heardBeatTimeoutMilliseconds"')
    }

    public async prepare(eventHandler: ServerHostEvents): Promise<void> {
        await this.backend.prepare(new ServerHostEvents(
            async () => {
                this.timerId = setInterval(this.onPeriodic.bind(this), this.heartBeatPeriodicTimeMillisecond)
                await eventHandler.onReady()
            },
            async bridge => {
                const keepAliveBridgeHost = new KeepAliveRpcBridgeHost(bridge)
                keepAliveBridgeHost.implementationDelegate = new ServerSideImplementationDelegate(
                    keepAliveBridgeHost,
                    async () => {
                        this.bridges.set(keepAliveBridgeHost.id, keepAliveBridgeHost)
                    },
                    async () => {
                        this.bridges.delete(keepAliveBridgeHost.id)
                        // console.log("Client deleted from SERVER HOST")
                    }
                )
                await keepAliveBridgeHost.setHandshakeConfig(new HandShakeMessage(this.heartBeatPeriodicTimeMillisecond, this.heardBeatTimeoutMilliseconds))
                await eventHandler.onClientConnected(keepAliveBridgeHost)
            },
            eventHandler.onError,
            async () => {
                clearInterval(this.timerId)
                await eventHandler.onShutdown()
            }
        ))
    }
    public async launch(): Promise<void> {
        await this.backend.launch()
    }
    public async shutdown(): Promise<void> {
        await this.backend.shutdown()
    }
    private async onPeriodic() {
        const promises: Promise<void>[] = []
        for (const [key, bridge] of this.bridges) {
            const delegate = bridge.implementationDelegate as ServerSideImplementationDelegate
            promises.push(delegate.sendPingMessage())
        }
        // console.log("Waiting...")
        for (const promise of promises) {
            try {await promise} catch(error) {}
        }
        // console.log("Mark and sweep")
        setTimeout(this.onMarkAndSweep.bind(this), this.heardBeatTimeoutMilliseconds * 2)
    }
    private async onMarkAndSweep() {
        this.bridges.forEach(async bridge => {
            const delegate = bridge.implementationDelegate as ServerSideImplementationDelegate
            if (delegate.isConnectionCorrespondToBeClosedOnServerSide) {
                // console.log("Corresponds close client")
                await bridge.close()
            }
        })
    }
}
interface IHostImplementationDelegate {
    onReady(): Promise<void>
    onClosed(): Promise<void>
    onPeriodic(): Promise<void>
    onMessageReceived(message: Message): Promise<void>
}
function unixTimeToDate(unixTime: number) {
    const date = new Date()
    date.setTime(unixTime)
    return date
}
class ServerSideImplementationDelegate implements IHostImplementationDelegate {
    private onReadyCompleter = Completer.create()
    public get isConnectionCorrespondToBeClosedOnServerSide(): boolean {
        const value = this.bridge.lastTrafficTime + this.bridge.HandshakeConfig.heartBeatPeriodicTimeMillisecond + (this.bridge.HandshakeConfig.heardBeatTimeoutMilliseconds * 2) < Date.now()
        // if (value) {
        //     console.log("Disconnected")
        // }
        return value
    }
    public constructor(private bridge: KeepAliveRpcBridgeHost, private onReadyAction: () => Promise<void>, private onClosedAction: () => Promise<void>) {

    }
    public async onReady(): Promise<void> {
        this.onReadyCompleter.complete()
        await this.onReadyAction()
        await this.sendHandshakeMessage()
    }
    public async onClosed(): Promise<void> {
        await this.onClosedAction()
    }
    public async onPeriodic() {

    }
    public async onMessageReceived(message: Message) {
        if (message.messageType == MessageType.PONG) await this.receivePongMessage(message)
    }
    private async sendHandshakeMessage() {
        const fragment = new Fragment(this.bridge.defaultTextEncoder.encode(JSON.stringify(this.bridge.HandshakeConfig)))
        await this.onReadyCompleter.promise
        await this.bridge.backend.send(fragment.encode())
    }
    private async receivePongMessage(message: PongMessage) {
        this.bridge.lastTrafficTime = Date.now()
        // console.log(`On pong message received by client side ${unixTimeToDate(this.bridge.lastTrafficTime)}`)
    }
    public async sendPingMessage() {
        const payload = new Fragment(this.bridge.defaultTextEncoder.encode(JSON.stringify(new PingMessage())))
        await this.bridge.backend.send(payload.encode())
    }
}
class ClientSideImplementationDelegate implements IHostImplementationDelegate {
    private timerId: NodeJS.Timeout = null!
    public get isConnectionCorrespondToBeClosed(): boolean {
        const now = Date.now()
        const value = this.bridge.lastTrafficTime + this.bridge.HandshakeConfig.heartBeatPeriodicTimeMillisecond + this.bridge.HandshakeConfig.heardBeatTimeoutMilliseconds < now
        // if (value) {
        //     console.log(`Closed ${unixTimeToDate(now)}::${unixTimeToDate(this.bridge.lastTrafficTime)}`)
        // }
        return value
    }
    public constructor(private bridge: KeepAliveRpcBridgeHost) {}
    public async onReady(): Promise<void> {
        
    }
    public async onClosed(): Promise<void> {
        clearInterval(this.timerId)
    }
    public async onPeriodic() {
        // console.log(`Periodic: ${unixTimeToDate(this.bridge.lastTrafficTime)}`)
        if (!this.isConnectionCorrespondToBeClosed) return
        await this.bridge.close()
    }
    public async onMessageReceived(message: Message): Promise<void> {
        if (message.messageType == MessageType.HANDSHAKE) await this.receiveHandshakeMessage(message as HandShakeMessage)
        if (message.messageType == MessageType.PING) await this.receivePingMessage(message)
    }
    private async receivePingMessage(message: PingMessage) {
        this.bridge.lastTrafficTime = Date.now()
        // console.log(`Ping message received by server, this time is: ${unixTimeToDate(this.bridge.lastTrafficTime)}`)
        const fragment = new Fragment(this.bridge.defaultTextEncoder.encode(JSON.stringify(new PongMessage)))
        await this.bridge.backend.send(fragment.encode())
    }
    private async receiveHandshakeMessage(message: HandShakeMessage) {
        this.bridge.setHandshakeConfig(message)
        this.timerId = setInterval(this.onPeriodic.bind(this), this.bridge.HandshakeConfig.heartBeatPeriodicTimeMillisecond)
        this.bridge.lastTrafficTime = Date.now()
        // console.log("Handshake")
    }
}

export class KeepAliveRpcBridgeHost implements IRpcBridgeHost<IRpcBridgeHost<any>> {
    public id = uuid()
    implementationDelegate: IHostImplementationDelegate = new ClientSideImplementationDelegate(this)
    public HandshakeConfig: HandShakeMessage = null!
    public constructor (public backend: IRpcBridgeHost<any>) {}
    public lastTrafficTime = Date.now() //both used to service side and client side, client side is updated when new message received or ping message received, on server side, only update pong message received.
    public defaultTextEncoder = new TextEncoder()
    public defaultTextDecoder = new TextDecoder()

    public async prepare(eventHandler: BridgeHostEvents): Promise<void> {
        await this.backend.prepare(new BridgeHostEvents(
            async () => {
                await eventHandler.onReady()
                await this.implementationDelegate.onReady()
            },
            eventHandler.onError,
            async () => {
                await eventHandler.onClose()
                await this.implementationDelegate.onClosed()
            },
            eventHandler.onUnableToConnect,
            async data => {
                const fragments = [...Fragment.fromBytes(data)]
                const message = JSON.parse(this.defaultTextDecoder.decode(fragments[0].data)) as Message
                if (message.messageType == MessageType.DATA_STREAM)
                    await eventHandler.onMessage(fragments[1].data)
                else {
                    this.implementationDelegate.onMessageReceived(message)
                }
            },
        ))
    }
    public async open(): Promise<void> {
        await this.backend.open()
    }
    public async close(): Promise<void> {
        await this.backend.close()
    }
    public async send(data: Uint8Array): Promise<void> {
        const payload = Fragment.encodeAll(
            new Fragment(this.defaultTextEncoder.encode(JSON.stringify(new DataStreamMessage()))),
            new Fragment(data)
        )
        await this.backend.send(payload)
    }
    public async setHandshakeConfig(message: HandShakeMessage) {
        this.HandshakeConfig = message
    }
}

enum MessageType {
    HANDSHAKE = 0x01,
    PING = 0x02,
    PONG = 0x03,
    DATA_STREAM = 0x04,
}
abstract class Message {
    time: number = Date.now()
    constructor(public messageType: MessageType) {}
}
class HandShakeMessage extends Message {
    public constructor(public heartBeatPeriodicTimeMillisecond: number, public heardBeatTimeoutMilliseconds: number) {
        super(MessageType.HANDSHAKE)
    }
}
class PingMessage extends Message {
    public constructor() {
        super(MessageType.PING)
    }
}
class PongMessage extends Message {
    public constructor() {
        super(MessageType.PONG)
    }
}
class DataStreamMessage extends Message {
    public constructor() {
        super(MessageType.DATA_STREAM)
    }
}